01. iOS Crash的捕获
Crash分类
MacOS是一个类Unix的操作系统,内核由XNU构成,而XNU是基于NeXTSTEP和FreeBSD混合开发而成,系统架构图如下:
不同的层次会产生不同的Crash, 根据Crash的不同来源,可以分为以下四类:
- #Mach异常
最底层的内核级异常。用户态的开发者可以直接通过Mach API设置thread,task,host的异常端口,来捕获Mach异常。 - #Unix 信号
又称BSD 信号,如果开发者没有捕获Mach异常,则会被host层的方法ux_exception()将异常转换为对应的UNIX信号,并通过方法threadsignal()将信号投递到出错线程。可以通过方法signal(x, SignalHandler)来捕获signal。 - #NSException
应用级异常,它是未被捕获的Objective-C异常,导致程序向自身发送了SIGABRT信号而崩溃,是app自己可控的,对于未捕获的Objective-C异常,是可以通过try catch来捕获的,或者通过NSSetUncaughtExceptionHandler()机制来捕获 - #C++异常
Mach异常
内核层的异常。用户态开发者可以通过 Mach API 设置 thread
、task
、hot
的异常端口来捕获 Mach 异常。
tasks
:资源所有权单位。每个任务由一个虚拟地址空间、一个端口权限名称控件、一个或多个线程组成。(类似于进程)threads
:任务中 CPU 执行的单位ports
:安全的单工通信通道,只能通过发生和接收功能进行访问。
这些内核对象,对于Mach来说都是一个个的Object,这些Objects基于Mach实现自己的功能,并通过Mach Message来进行通信,Mach提供了相关的应用层的API来操作。
与 Mach
异常相关的 API 有:
task_get_exception_ports
:获取 task 的异常端口task_set_exception_ports
:设置 task 的异常端口mach_port_allocate
:创建调用者指定的端口权限类型mach_port_insert_right
:将指定的端口插入目标 task
如何捕获Mach异常
要捕获Mach异常,需要新建一个监控线程,在监控线程中监听 Mach 异常并处理异常信息。
static mach_port_t server_port;
static void *exc_handler(void *ignored);
//判断是否 Xcode 联调
bool ksdebug_isBeingTraced(void)
{
struct kinfo_proc procInfo;
size_t structSize = sizeof(procInfo);
int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()};
if(sysctl(mib, sizeof(mib)/sizeof(*mib), &procInfo, &structSize, NULL, 0) != 0)
{
return false;
}
return (procInfo.kp_proc.p_flag & P_TRACED) != 0;
}
#define EXC_UNIX_BAD_SYSCALL 0x10000 /* SIGSYS */
#define EXC_UNIX_BAD_PIPE 0x10001 /* SIGPIPE */
#define EXC_UNIX_ABORT 0x10002 /* SIGABRT */
static int signalForMachException(exception_type_t exception, mach_exception_code_t code)
{
switch(exception)
{
case EXC_ARITHMETIC:
return SIGFPE;
case EXC_BAD_ACCESS:
return code == KERN_INVALID_ADDRESS ? SIGSEGV : SIGBUS;
case EXC_BAD_INSTRUCTION:
return SIGILL;
case EXC_BREAKPOINT:
return SIGTRAP;
case EXC_EMULATION:
return SIGEMT;
case EXC_SOFTWARE:
{
switch (code)
{
case EXC_UNIX_BAD_SYSCALL:
return SIGSYS;
case EXC_UNIX_BAD_PIPE:
return SIGPIPE;
case EXC_UNIX_ABORT:
return SIGABRT;
case EXC_SOFT_SIGNAL:
return SIGKILL;
}
break;
}
}
return 0;
}
static NSString *stringForMachException(exception_type_t exception) {
switch(exception)
{
case EXC_ARITHMETIC:
return @"EXC_ARITHMETIC";
case EXC_BAD_ACCESS:
return @"EXC_BAD_ACCESS";
case EXC_BAD_INSTRUCTION:
return @"EXC_BAD_INSTRUCTION";
case EXC_BREAKPOINT:
return @"EXC_BREAKPOINT";
case EXC_EMULATION:
return @"EXC_EMULATION";
case EXC_SOFTWARE:
{
return @"EXC_SOFTWARE";
break;
}
}
return 0;
}
void installExceptionHandler() {
if (ksdebug_isBeingTraced()) {
// 当前正在调试状态, 不启动 mach 监听
return ;
}
kern_return_t kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &server_port);
assert(kr == KERN_SUCCESS);
kern_return_t rc = 0;
exception_mask_t excMask = EXC_MASK_BAD_ACCESS |
EXC_MASK_BAD_INSTRUCTION |
EXC_MASK_ARITHMETIC |
EXC_MASK_SOFTWARE |
EXC_MASK_BREAKPOINT;
rc = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &server_port);
if (rc != KERN_SUCCESS) {
fprintf(stderr, "------->Fail to allocate exception port\\\\\\\\n");
return;
}
rc = mach_port_insert_right(mach_task_self(), server_port, server_port, MACH_MSG_TYPE_MAKE_SEND);
if (rc != KERN_SUCCESS) {
fprintf(stderr, "-------->Fail to insert right");
return;
}
rc = thread_set_exception_ports(mach_thread_self(), excMask, server_port, EXCEPTION_DEFAULT, MACHINE_THREAD_STATE);
if (rc != KERN_SUCCESS) {
fprintf(stderr, "-------->Fail to set exception\\\\\\\\n");
return;
}
//建立监听线程
pthread_t thread;
pthread_create(&thread, NULL, exc_handler, NULL);
}
static void *exc_handler(void *ignored) {
// Exception handler – runs a message loop. Refactored into a standalone function
// so as to allow easy insertion into a thread (can be in same program or different)
mach_msg_return_t rc;
fprintf(stderr, "Exc handler listening\\\\\\\\n");
// The exception message, straight from mach/exc.defs (following MIG processing) // copied here for ease of reference.
typedef struct {
mach_msg_header_t Head;
/* start of the kernel processed data */
mach_msg_body_t msgh_body;
mach_msg_port_descriptor_t thread;
mach_msg_port_descriptor_t task;
/* end of the kernel processed data */
NDR_record_t NDR;
exception_type_t exception;
mach_msg_type_number_t codeCnt;
integer_t code[2];
int flavor;
mach_msg_type_number_t old_stateCnt;
natural_t old_state[144];
} Request;
Request exc;
struct rep_msg {
mach_msg_header_t Head;
NDR_record_t NDR;
kern_return_t RetCode;
} rep_msg;
for(;;) {
// Message Loop: Block indefinitely until we get a message, which has to be
// 这里会阻塞,直到接收到exception message,或者线程被中断。
// an exception message (nothing else arrives on an exception port)
rc = mach_msg( &exc.Head,
MACH_RCV_MSG|MACH_RCV_LARGE,
0,
sizeof(Request),
server_port, // Remember this was global – that's why.
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
if(rc != MACH_MSG_SUCCESS) {
/*... */
break ;
};
//Mach Exception 类型
NSMutableString *crashInfo = [NSMutableString stringWithFormat:@"mach exception:%@ %@\n\n",stringForMachException(exc.exception), stringForSignal(signalForMachException(exc.exception, exc.code[0]))];
rep_msg.Head = exc.Head;
rep_msg.NDR = exc.NDR;
rep_msg.RetCode = KERN_FAILURE;
kern_return_t result;
if (rc == MACH_MSG_SUCCESS) {
result = mach_msg(&rep_msg.Head,
MACH_SEND_MSG,
sizeof (rep_msg),
0,
MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
}
//移除其他 Crash 监听, 防止死锁
NSSetUncaughtExceptionHandler(NULL);
signal(SIGHUP, SIG_DFL);
signal(SIGINT, SIG_DFL);
signal(SIGQUIT, SIG_DFL);
signal(SIGABRT, SIG_DFL);
signal(SIGILL, SIG_DFL);
signal(SIGSEGV, SIG_DFL);
signal(SIGFPE, SIG_DFL);
signal(SIGBUS, SIG_DFL);
signal(SIGPIPE, SIG_DFL);
}
return NULL;
}
注意:避免在 Xcode 联调时监听,会死锁。原因是我们监听了EXC_BREAKPOINT
这类型的Exception
,一旦启动 app 联调后, 会立即触发EXC_BREAKPOINT
。而这段代码处理完后,会进入下一个循环等待,可主线程这是还等着消息处理结果,这就造成等待死锁。
Unix 信号
Mach已经通过异常机制提供了底层的异常处理,但为了兼容更为流行的POSIX标准,BSD在Mach异常机制之上构建的UNIX信号处理机制。
UNIX信号又称 BSD
信号,如果开发者没有捕获 Mach 异常,则会被 host 层的方法 ux_exception()
将异常转换为对应的 Unix 信号,并通过方法 threadsignal()
将信号投递到出错线程。可以同 signal(x, SignalHandler)
来捕获 signal
。
UNIX信号列表
UNIX信号实际上是由Mach port抛出的信号转化的,一共有31种信号
Tips: 在终端输入
kill -l
查看所有的 signal 信号。
信号 | 含义 |
---|---|
SIGHUP | 挂起 |
SIGINT | 程序终止信号 interrupt,在用户键入 INTR 字符(通常是 Ctrl-C)是发出,用于通知前台进程组终止进程 |
SIGQUIT | 程序退出信号 quit,由 QUIT 字符来控制(通常是Ctrl-),程序在收到该信号退出时会生成 core 文件 |
SIGILL | 执行非法指令,当一个进程尝试执行一个非法指令时发送给它的信号 |
SIGTRAP | 由断点指令或陷阱指令 |
SIGABRT | 程序打断信号 abort |
SIGFPE | 致命的算术运算错误 |
SIGKILL | 立即结束程序的运行。不能被阻塞、处理和忽略 |
SIGBUS | 非法地址 |
SIGSEGV | 无效内存访问 |
SIGSYS | 非法的系统调用 |
SIGPIPE | 管道破裂。进程间的通信,如管道的异常读写 |
SIGALRM | alarm 发出的信号 |
SIGTERM | 终止信号,可被阻塞和处理。通常用来要求程序自己正常退出 |
SIGURG | I/O有紧急数据达到当前进程 |
SIGSTOP | 进程停止 |
SIGTSTP | 进程停止 |
SIGCONT | 进程继续 |
SIGCHLD | 子进程退出 |
SIGTTIN | 进程停止,后台进程从终端读数据时 |
SIGTTOU | 进程停止,后台进程想终端写数据时 |
SIGIO | I/O相关 |
SIGXCPU | 进程的CPU时间篇到期 |
SIGXFSZ | 文件大小超出上限 |
SIGVTALRM | 虚拟时钟超时 |
SIGPROF | profile 时钟超时 |
SIGWINCH | 窗口大小改变 |
SIGUSR1 | 用户信号1 |
SIGUSR2 | 用户信号2 |
SIGSTKFLT | 协处理器栈故障,栈溢出,在内存耗尽时,一般 malloc 返回 NULL 且设置 errno 为 ENOMEM,但有些系统可能会使用 SIGSTKFLT 信号代替 |
SIGPWR | 关机 |
如何捕获Unix信号
捕获信号:
// 一般需要捕获的信号
static const int g_fatalSignals[] = {
SIGABRT,
SIGBUS,
SIGFPE,
SIGILL,
SIGPIPE,
SIGSEGV,
SIGSYS,
SIGTRAP,
};
void installSignalHandler() {
stack_t ss;
struct sigaction sa;
struct timespec req, rem;
long ret;
// 申请一块内存空间作为可选的信号处理函数栈使用
ss.ss_flags = 0;
ss.ss_size = SIGSTKSZ;
ss.ss_sp = malloc(ss.ss_size);
// 使用 sigaltstack 函数通知系统可选的信号处理栈帧的存在及其位置
sigaltstack(&ss, NULL);
// 指定 SA_ONSTACK 标志通知系统这个信号处理函数应该在可选的栈帧上面执行注册的信号处理函数
memset(&sa, 0, sizeof(sa));
sa.sa_handler = handleSignalException;
sa.sa_flags = SA_ONSTACK;
sigaction(SIGABRT, &sa, NULL);
}
void XXXHandleSignalException(int signal) {
// 打印堆栈
NSMutableString *crashInfo = [[NSMutableString alloc] init];
[crashInfo appendString:[NSString stringWithFormat:@"signal:%d\n",signal]];
[crashInfo appendString:@"Stack:\n"];
void* callstack[128];
int i, frames = backtrace(callstack, 128);
char** strs = backtrace_symbols(callstack, frames);
for (i = 0; i <frames; ++i) {
[crashInfo appendFormat:@"%s\n", strs[I]];
}
NSLog(@"%@", crashInfo);
// 移除其他 Crash 监听, 防止死锁
NSSetUncaughtExceptionHandler(NULL);
signal(SIGHUP, SIG_DFL);
signal(SIGINT, SIG_DFL);
signal(SIGQUIT, SIG_DFL);
signal(SIGABRT, SIG_DFL);
signal(SIGILL, SIG_DFL);
signal(SIGSEGV, SIG_DFL);
signal(SIGFPE, SIG_DFL);
signal(SIGBUS, SIG_DFL);
signal(SIGPIPE, SIG_DFL);
}
NSException
应用层的异常,未被捕获的异常,导致程序向自身发送了 SIGABRT
信号而崩溃,是应用程序自己可控的。对于未被捕获的异常,是可以通过 try-catch
或 NSSetUncaughtExceptionHandler()
机制类捕获的。
常见的 Exception:
NSInvalidArgumentException
:非法参数异常。加强对参数的检查,避免传入非法参数,特别是标记为 nonull 的参数。NSRangeException
:越界异常NSGenericException
:遍历的同时对原集合进行修改NSInternalInconsistencyException
:不一致异常。如NSDictionary
当NSMutableNSDictionary
使用。NSFileHandleOperationException
:文件处理异常。常见的是存储空间不足NSMallocException
:内存异常。如内存不足。
系统定义的所有Exception
见 NSExceptionName
捕获 NSExpection:
// 记录之前的Crash回调函数(如果有的话)
static NSUncaughtExceptionHandler *previousUncaughtExceptionHandler = NULL;
+ (void)registerUncaughtExceptionHandler {
// 将别人之前注册的Crash回调取出并备份
previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
// 然后再注册自己的
NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
}
// 崩溃时的回调函数
static void UncaughtExceptionHandler(NSException * exception) {
// 异常的堆栈信息
NSArray *stackInfo = [exception callStackSymbols];
// 出现异常的原因
NSString *reason = [exception reason];
// 异常名称
NSString *name = [exception name];
// 异常错误报告
NSString *exceptionInfo = [NSString stringWithFormat:@"uncaughtException异常错误报告:\n name:%@\n reason:\n %@\n callStackSymbols:\n %@", name, reason, [stackInfo componentsJoinedByString:@"\n"]];
// 保存Crash日志到沙盒cache目录
[SKTool cacheCrashLog:exceptionInfo name:@"CrashLog(UncaughtException)"];
// 在自己handler处理完后记得把别人的handler注册回去,形成规范的SOP
if (previousUncaughtExceptionHandler) {
previousUncaughtExceptionHandler(exception);
}
// 杀掉程序,这样可以防止同时抛出的SIGABRT被Signal异常捕获
kill(getpid(), SIGKILL);
}
C++异常
系统捕获到 C++ 异常后会将其转换为 OC 异常抛出,此时的调用堆栈是在异常发生时的队长;但若转换失败则会调用 __cxa_throw
抛出异常,此时的调用队长是处理异常的堆栈,导致原始异常调用堆栈丢失。 捕获 C++ 异常:
- 设置异常处理函数:
g_originalTerminateHandler = std::set_terminate(CPPExceptionTerminate);
调用 set_terminate(CPPExceptionTerminate)
设置新的全局终止处理函数并保持旧的函数。
- 重写
__cxa_throw
void __cxa_throw(void* thrown_exception, std::type_info* tinfo, void (*dest)(void*)) { // 获取调用堆栈并存储 // 再调用原始的 __cxa_throw 函数 }
- 异常处理函数
__cxa_throw
往后执行,进入set_terminate
设置的异常梳理函数。判断如果是 OC 异常则什么也不多,让 OC 异常机制处理;否则获取异常信息